package com.demo.nio.server;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;

/**
 * Created by hunter
 *
 * @Description BIO对应linux的阻塞模型, 阻塞IO在accept、read和write的调用方法时都会阻塞等待, 造成线程啥也没干,浪费线程资源, 一个IO连接就得开一个线程
 *              NIO对应linux的IO多路复用模型, 它先将所有IO连接注册到监听器上, 调用底层函数监听IO们的事件, 有事件后才开始正在的accept、read和write（这里和BIO差不多，都是调用的同一个方法比如read是调用的linux的recv方法）, 一个请求得开一个线程
 *              在读写上NIO并不比BIO快，就是并发量比BIO高，因为大量的线程资源得到了很好的运用。在并发比较少的情况下用BIO好一些，代码也不复杂易懂
 */
public class NioServer implements Runnable {

    private static final String CHARSET_NAME = "UTF-8";

    private static final int PORT = 8000;

    private static final int BUFF_SIZE = 1024;

    private void startServer() throws IOException {
        // 开启一个NIO服务端
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置监听端口
        serverSocketChannel.bind(new InetSocketAddress(PORT));
        // 设置非阻塞
        serverSocketChannel.configureBlocking(false);

        // 开启一个选择器, 作为IO复用的第一步
        Selector selector = Selector.open();
        // 将服务端IO 注册到选择器上, 服务端必须监听OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 开始循环监听注册到选择器上的IO, 这里的select方法是阻塞的, 其实就是调用linux的epoll/windows的IOCP/mac的kqueue
        // 在服务端IO可用的情况下, 这里的select都是大于0的
        while (selector.select() > 0) {
            // 当发现有IO事件时，会将其放入selector的selectedKeys即set中
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                // 拿到某个端口传过来的事件, 开始处理, 作为IO复用的第二步
                SelectionKey selectionKey = iterator.next();

                // 如果是连接事件, 考虑用多线程去处理
                if (selectionKey.isAcceptable()) {
                    System.out.println("is acceptable ...");

                    // 这里的channel是NIO服务端
                    ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
                    // 这个channel才是客户端, 这里等同于BIO的accept, 是阻塞的
                    SocketChannel socketChannel = server.accept();
                    if (socketChannel == null) {
                        continue;
                    }
                    // 设置该客户端管道非阻塞, 对应linux的非阻塞模型
                    socketChannel.configureBlocking(false);
                    // 注册 这个客户端的相关IO到 selector上, 表明监听事件类型, 此轮过后就会被加入监听
                    socketChannel.register(selector, SelectionKey.OP_READ);

                    // 这里必须处理异常, 不然监听线程就GG了
                    try {
                        // 一个简单的写回, 这里等同于BIO的write, 是阻塞的 Direct Buffer尽量复用,因为创建和销毁的代价比较高
                        ByteBuffer buffer = ByteBuffer.allocateDirect(BUFF_SIZE);
                        buffer.put("Register channel success ...".getBytes());
                        buffer.flip();
                        socketChannel.write(buffer);
                    } catch (Exception e) {
                        e.printStackTrace();
                        socketChannel.close();
                    }
                }

                // 如果是可读, 即有数据过来, 考虑用多线程去处理
                if (selectionKey.isReadable()) {
                    System.out.println("is readable ...");

                    // 建立变量准备去读过来的请求 Direct Buffer尽量复用,因为创建和销毁的代价比较高
                    ByteBuffer inBuffer = ByteBuffer.allocateDirect(BUFF_SIZE);
                    // 拿到对应客户端
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();

                    // 这里必须处理异常, 不然监听线程就GG了
                    try {
                        // 这里等同于BIO的read, 是阻塞的
                        socketChannel.read(inBuffer);
                        // read完后指针是指在后面的, 要重新读就要重新拨回指针到0
                        inBuffer.flip();
                        String receiveData = Charset.forName(CHARSET_NAME).decode(inBuffer).toString();

                        System.out.println("Received data : " + receiveData);

                        // 简单的写回, 这里等同于BIO的write, 是阻塞的 Direct Buffer尽量复用,因为创建和销毁的代价比较高
                        ByteBuffer outBuffer = ByteBuffer.allocateDirect(BUFF_SIZE);
                        outBuffer.put(("Received data : " + receiveData + " success ...").getBytes());
                        outBuffer.flip();
                        socketChannel.write(outBuffer);
                    } catch (Exception e) {
                        e.printStackTrace();
                        socketChannel.close();
                    }
                }

                // 处理完移除, 因为遍历的是保存在selector中的set事件集合, 他是永远不会去移除的, 所以你处理消费完之后要移除它, 不要会重复处理消费
                iterator.remove();
            }
        }
    }

    @Override
    public void run() {
        try {
            startServer();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
